Chapter 6 Data Visualizations

6.1 Room Temperature Reduction

6.1.1 Task

As part of an energy optimization, you lower the room temperatures in a room and would now like to show the reduction effect using the time series of the room temperature sensor. In the example below you make two optimizations at different dates.

You want to create a time series plot with

  • the daily median, min and max value

  • the overall median of each period

  • the desired setpoint

6.1.2 Basis

  • Time series data from e.g. a temperature sensor with unaligned time intervals

6.1.3 Solution

library(dplyr)
library(lubridate)
library(dygraphs)
library(xts)
library(redutils)
library(RColorBrewer)

# Settings
tempSetpoint = 22.0

startDate = "2018-11-01"
endDate = "2019-02-01"

optiDate1 = "2018-12-17"
optiLabel1 = "Optimization I"

optiDate2 = "2019-01-03"
optiLabel2 = "Optimization II"

optiDelayDays = 5

# read and print data
df <- read.csv("https://github.com/hslu-ige-laes/edar/raw/master/sampleData/flatTempHum.csv",
                 stringsAsFactors=FALSE,
                 sep =";")

# select temperature and remove empty cells
df <- df %>% select(time, FlatA_Temp) %>% na.omit()

# create column with day for later grouping
df$time <- parse_date_time(df$time, "YmdHMS", tz = "Europe/Zurich")
df$day <- as.Date(cut(df$time, breaks = "day"))
df$day <- as.Date(as.character(df$day,"%Y-%m-%d"))

# filter time range
df <- df %>% filter(day > startDate, day < endDate)

# calculate daily median, min and max of temperature
df <- df %>%
  group_by(day) %>%
  mutate(minDay = min(as.numeric(FlatA_Temp)),
         medianDay = median(as.numeric(FlatA_Temp)),
         maxDay = max(as.numeric(FlatA_Temp))
         ) %>%
  ungroup()

# shrink down to daily values and remove rows with empty values
df <- df %>% select(day, medianDay, minDay, maxDay) %>% unique() %>% na.omit()

# calculate medians for time ranges
df <- df %>%
  mutate(period = ifelse(day >= startDate & day <= optiDate1,
                         "Baseline",
                         ifelse((day >= (as.Date(optiDate1) + optiDelayDays))
                                & (day <= optiDate2),
                                "Opti1",
                                ifelse((day >= (as.Date(optiDate2) + optiDelayDays))
                                & (day <= endDate),
                                "Opti2",
                                NA)
                         )))

df <- df %>%
  group_by(period) %>%
  mutate(medianPeriod = ifelse(is.na(period), NA, median(medianDay))) %>% 
  ungroup() %>% 
  select(-period)

# create xts object for plotting
plotdata <- xts( x=df[,-1], order.by=df$day)

# plot graph
dygraph(plotdata, main = "Room Temperature Reduction") %>%
  dyAxis("x", drawGrid = FALSE) %>%
  dySeries(c("minDay", "medianDay", "maxDay"),
           label = "Temperature") %>%
  dySeries(c("medianPeriod"),
           label = "Median Period",
           strokePattern = "dashed") %>%
  dyOptions(colors = RColorBrewer::brewer.pal(3, "Set2")) %>%
  dyEvent(x = optiDate1,
          label = optiLabel1,
          labelLoc = "bottom",
          color = "slategray",
          strokePattern = "dotted") %>% 
  dyEvent(x = optiDate2,
          label = optiLabel2,
          labelLoc = "bottom",
          color = "slategray",
          strokePattern = "dotted") %>% 
  dyLimit(tempSetpoint,
          color = "red",
          label = "Target Setpoint") %>% 
  dyRangeSelector() %>% 
  dyLegend(show = "always")

6.1.4 Discussion

In this example we used the dygraph package to create the graph. This package is fast and allows to show a rangeslider on the bottom of the graph. The exact same graph but without a slider is as well possible with ggplot.

Please note that the calculation of the periodic median after optimization I and II starts delayed because it takes time until the building has cooled down.

6.2 Building Energy Signature

6.2.1 Task

You want to create a scatter plot with

  • the daily mean outside temperature on the x-axis

  • the daily energy consumption on the y-axis

  • points colored according to season

6.2.2 Basis

  • Two separate csv files with time series data from the outside temperature and the energy data with unaligned time intervals

  • Energy consumption time series from a energy meter with steadily increasing meter values

6.2.3 Solution

After reading in the two time series the data has to get aggregated per day and then merged. Note that during the aggregation of the energy data you have to calculate the daily conspumption from the steadiliy increasing meter values as well.

Create a new script, copy/paste the following code and run it:

library(ggplot2)
library(plotly)
library(dplyr)
library(redutils)
library(lubridate)

# load time series data and aggregate daily mean values
dfOutsideTemp <- read.csv("https://github.com/hslu-ige-laes/edar/raw/master/sampleData/centralOutsideTemp.csv",
                          stringsAsFactors=FALSE,
                          sep =";")

dfOutsideTemp$time <- parse_date_time(dfOutsideTemp$time,
                                      order = "YmdHMS",
                                      tz = "Europe/Zurich")

dfOutsideTemp$day <- as.Date(cut(dfOutsideTemp$time, breaks = "day"))

dfOutsideTemp <- dfOutsideTemp %>%
  group_by(day) %>%
  mutate(tempMean = mean(centralOutsideTemp)) %>%
  ungroup()

dfOutsideTemp <- dfOutsideTemp %>%
  select(day, tempMean) %>%
  unique() %>%
  na.omit()

dfHeatEnergy <- read.csv("https://github.com/hslu-ige-laes/edar/raw/master/sampleData/centralHeating.csv",
                         stringsAsFactors=FALSE,
                         sep =";")

dfHeatEnergy <- dfHeatEnergy %>%
  select(time, energyHeatingMeter) %>%
  na.omit()

dfHeatEnergy$time <- parse_date_time(dfHeatEnergy$time,
                                     orders = "YmdHMS",
                                     tz = "Europe/Zurich")

dfHeatEnergy$day <- as.Date(cut(dfHeatEnergy$time, breaks = "day"))

dfHeatEnergy <- dfHeatEnergy %>%
  group_by(day) %>%
  mutate(energyMax = max(energyHeatingMeter)) %>%
  ungroup()

dfHeatEnergy <- dfHeatEnergy %>%
  select(day, energyMax) %>%
  unique() %>%
  na.omit()

dfHeatEnergy <- dfHeatEnergy %>% 
  mutate(energyCons = energyMax - lag(energyMax)) %>%
  select(-energyMax) %>%
  na.omit()

# merge the data in a tidy format
df <- merge(dfOutsideTemp, dfHeatEnergy, by = "day")

# calculate season
df <- df %>% mutate(season = redutils::getSeason(df$day))

# static chart with ggplot
p <- ggplot2::ggplot(df) +
  ggplot2::geom_point(aes(x = tempMean,
                          y = energyCons,
                          color = season,
                          alpha = 0.1,
                          text = paste("</br>Date:  ", as.Date(df$day),
                                       "</br>Temp: ", round(df$tempMean, digits = 1), "\u00B0C",
                                       "</br>Energy: ", round(df$energyCons, digits = 0), "kWh/d",
                                       "</br>Season: ", df$season))
                      ) +
  scale_color_manual(values=c("#440154", "#2db27d", "#fde725", "#365c8d")) +
  ggtitle("Building Energy Signature") +
        theme_minimal() +
        theme(
          legend.position="none",
          plot.title = element_text(hjust = 0.5)
        )

# interactive chart
plotly::ggplotly(p, tooltip = c("text")) %>%
  layout(xaxis = list(title = "Outside temperature (\u00B0C)",
                      range = c(min(-5,min(df$tempMean)), max(35,max(df$tempMean))), zeroline = F),
         yaxis = list(title = "Daily energy consumption (kWh/d)",
                      range = c(-5, max(df$energyCons) + 10)),
         showlegend = TRUE
         ) %>%
  plotly::config(displayModeBar = FALSE, displaylogo = FALSE)

6.3 Daily Energy Profiles

## [1] "English_United States.1252"
library(plotly)
library(dplyr)
library(lubridate)

# load time series data
df <- read.csv("https://github.com/hslu-ige-laes/edar/raw/master/sampleData/eboBookEleMeter.csv",
               stringsAsFactors=FALSE,
               sep =";")

# rename column names
colnames(df) <- c("timestamp", "meterValue")

df$timestamp <- parse_date_time(df$timestamp,
                                orders = "YmdHMS",
                                tz = "Europe/Zurich")
df$timestamp <- force_tz(df$timestamp, tzone = "UTC")

# uncomment to filter time range if necessary
#df <- df %>% filter(timestamp > "2015-03-01 00:00:00", timestamp < "2015-04-01 00:00:00")

# Fill missing values with NA
grid.df <- data.frame(timestamp = seq(min(df$timestamp, na.rm = TRUE),
                                      max(df$timestamp, na.rm = TRUE),
                                      by = "15 mins"))
df <- merge(df, grid.df, all = TRUE)

# convert steadily counting energy meter value from kWh to power in kW
df <- df %>% 
  mutate(value = (meterValue - lag(meterValue))*4) %>%
  select(-meterValue) %>%
  na.omit()

# remove negative values which occur beause of change summer/winter time
df <- df %>% filter(value >= 0)

# add metadata for later grouping and visualization purposes
df$x <- hour(df$timestamp) + minute(df$timestamp)/60 + second(df$timestamp) / 3600
df$weekday <- weekdays(df$timestamp)
df$weekday <- factor(df$weekday, c("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday", "Sunday"))
df$day <- as.Date(df$timestamp, format = "%Y-%m-%d  %H:%M:%S")

df <- df %>% mutate(value = ifelse(x == 0.00, NA, df$value))

# plot graph with all time series
rangeX <- seq(0,24,0.25)
maxValue <- max(df$value, na.rm = TRUE)*1.05

df %>% 
  highlight_key(~day) %>%
  plot_ly(x=~x,
          y=~value,
          color=~weekday,
          type="scatter",
          mode="lines",
          line = list(width = 1),
          alpha = 0.15,
          colors = "dodgerblue4",
          text = ~day,
          hovertemplate = paste("Time: ", format(df$timestamp, "%H:%M"),
                                "<br>Date: ", format(df$timestamp, "%Y-%m-%d"),
                                "<br>Value: %{y:.0f}")) %>%
  # workaround with add_trace to have fixed y axis when selecting a dedicated day
  add_trace(x = 0, y = 0, type = "scatter", showlegend = FALSE, opacity=0) %>%
  add_trace(x = 24, y = maxValue, type = "scatter", showlegend = FALSE, opacity=0) %>%
  layout(title = "Superimposed Profiles of Power Consumption per 15 min",
         showlegend = TRUE,
         xaxis = list(
           title = "Hour of day",
           range = rangeX,
           tickvals = list(0, 3, 6, 9, 12, 15, 18, 21),
           showline=TRUE
           ),
           yaxis = list(
             title = "Power (kW)",
             range = c(0, maxValue)
           )
         ) %>% 
  highlight(on = "plotly_hover",
            off = "plotly_doubleclick",
            color = "orange",
            opacityDim = 1.0,
            selected = attrs_selected(showlegend = FALSE)) %>% # this hides elements in the legend
  plotly::config(modeBarButtons = list(list("toImage")), displaylogo = FALSE)

Next we want to create an overview with the mean values for each 15 minute slot per day.

Append the following code at the end of your script:

6.4 Mollier hx Diagram

6.4.1 Task

You want to plot a mollier h-x diagram with

  • scatter plot of temperature- and humidity sensor data (mean values per day)

  • points colored according to season

  • comfort zone

6.4.2 Basis

  • A csv file with time series from multiple temperature and humidity sensors in °C and %rH

6.4.3 Solution

The sensor data is not in a constant intervall and not yet aggregated. So after reading in the time series the data has to get filtered and aggregated per day.

Finally use the plot function mollierHxDiagram from the redutils package (R Energy Data Utilities). If you have not yet installed this package, proceed as follows:

Create a new script, copy/paste the following code and run it:

6.5 SIA 180 Thermal Comfort

6.5.1 Task

You want to plot a diagram like the one from the SIA 180:2014 which showes

  • scatter plot of indoor- and outdoor temperature sensor data (indoor mean of day, outdoor mean of last 48 hours)

  • points colored according to season

  • different comfort lines

6.5.2 Basis

  • A csv file with time series from multiple temperature and humidity sensors in °C

  • A csv file with the outdoor temperature

6.5.3 Solution

The sensor data is not in a constant intervall and not yet aggregated. So after reading in the time series the data has to get filtered, aggregated per day and merged.

Create a new script, copy/paste the following code and run it:

library(redutils)
library(dplyr)
library(lubridate)
library(zoo)
library(plotly)

# load time series data and aggregate mean values
dfTempOa <- read.csv("https://github.com/hslu-ige-laes/edar/raw/master/sampleData/centralOutsideTemp.csv",
                          stringsAsFactors=FALSE,
                          sep =";")

dfTempOa$time <- parse_date_time(dfTempOa$time,
                                      order = "YmdHMS",
                                      tz = "UTC")

dfTempOa$hour <- cut(dfTempOa$time, breaks = "hour")

dfTempOa <- dfTempOa %>%
  group_by(hour) %>%
  mutate(tempMean = mean(centralOutsideTemp)) %>%
  ungroup() %>% 
  select(time, tempMean) %>% 
  unique()

# Fill missing values with NA
grid.df <- data.frame(time = seq(min(dfTempOa$time, na.rm = TRUE),
                                 max(dfTempOa$time, na.rm = TRUE),
                                 by = "hour"))
dfTempOa <- merge(dfTempOa, grid.df, all = TRUE)

dfTempOa <- dfTempOa %>%
  mutate(tempOa = rollmean(tempMean, 48, fill = NA, align = "right"))

dfTempOa <- dfTempOa %>%
  select(time, tempOa) %>%
  unique() %>%
  na.omit()


dfTempR <- read.csv("https://github.com/hslu-ige-laes/edar/raw/master/sampleData/flatTempHum.csv",
                       stringsAsFactors=FALSE,
                       sep =";")

dfTempR$time <- parse_date_time(dfTempR$time,
                                   order = "YmdHMS",
                                   tz = "UTC")

# select temperature and humidity and remove empty cells
dfTempR <- dfTempR %>% select(time, FlatA_Temp) %>% na.omit()

dfTempR$hour <- cut(dfTempR$time, breaks = "hour")

dfTempR <- dfTempR %>%
  group_by(hour) %>%
  mutate(tempR = mean(FlatA_Temp)) %>%
  ungroup() %>% 
  select(time, tempR) %>% 
  unique()

# Fill missing values with NA
grid.df <- data.frame(time = seq(min(dfTempR$time, na.rm = TRUE),
                                 max(dfTempR$time, na.rm = TRUE),
                                 by = "hour"))
dfTempR <- merge(dfTempR, grid.df, all = TRUE)

data <- merge(dfTempR, dfTempOa, all = TRUE) %>% unique() %>% na.omit()

data$season <- redutils::getSeason(data$time)

# plot diagram

# axis properties
minx <- floor(min(-4, min(data$tempOa)))
maxx <- ceiling(max(32, max(data$tempOa)))

miny <- floor(min(18.0,min(data$tempR)))-1
maxy <- ceiling(max(32.0,max(data$tempR)))+1

# line setpoint heat
df.heatSp <- data.frame(tempOa = c(minx, 19, 23.5, maxx), tempR = c(20.5, 20.5, 22, 22))

# line setpoint cool according to SIA 180:2014 Fig. 4
df.coolSp1 <- data.frame(tempOa = c(minx, 12, 17.5, maxx),tempR = c(24.5, 24.5, 26.5, 26.5))

# line setpoint cool according to SIA 180:2014 Fig. 3
df.coolSp2 <- data.frame(tempOa = c(minx, 10, maxx),tempR = c(25, 25, 0.33 * maxx + 21.8))

data %>%
  plot_ly(showlegend = TRUE) %>%
  add_lines(data = df.coolSp2,
            x = ~tempOa,
            y = ~tempR,
            name = "Max. heated, cooled or mech. ventilated",
            opacity = 0.7,
            color = "#FDE725FF",
            hoverinfo = "text",
            text = ~ paste("Max. heated, cooled or mech. ventilated",
                           "<br />TempR:   ", sprintf("%.1f \u00B0C", tempR),
                           "<br />TempOa: ", sprintf("%.1f \u00B0C", tempOa)
            )
  ) %>%
  add_lines(data = df.coolSp1,
            x = ~tempOa,
            y = ~tempR,
            name = "Max. natural ventilation",
            opacity = 0.7,
            color = "#1E9B8AFF",
            hoverinfo = "text",
            text = ~ paste("Max. natural ventilation",
                           "<br />TempR:   ", sprintf("%.1f \u00B0C", tempR),
                           "<br />TempOa: ", sprintf("%.1f \u00B0C", tempOa)
            )        ) %>%
  add_lines(data = df.heatSp,
            x = ~tempOa,
            y = ~tempR,
            name = "Minimum", 
            opacity = 0.7,
            color = "#440154FF",
            hoverinfo = "text",
            text = ~ paste("Minimum",
                           "<br />TempR:   ", sprintf("%.1f \u00B0C", tempR),
                           "<br />TempOa: ", sprintf("%.1f \u00B0C", tempOa)
            )        ) %>%
  add_markers(data = data %>% filter(season == "Spring"),
              x = ~tempOa,
              y = ~tempR,
              name = "Spring",
              marker = list(color = "#2db27d", opacity = 0.2),
              hoverinfo = "text",
              text = ~ paste("TempR:   ", sprintf("%.1f \u00B0C", tempR),
                             "<br />TempOa: ", sprintf("%.1f \u00B0C", tempOa),
                             "<br />Date:       ", time,
                             "<br />Season:  ", season
              )
  ) %>%
  add_markers(data = data %>% filter(season == "Summer"),
              x = ~tempOa,
              y = ~tempR,
              name = "Summer",
              marker = list(color = "#febc2b", opacity = 0.2),
              hoverinfo = "text",
              text = ~ paste("TempR:   ", sprintf("%.1f \u00B0C", tempR),
                             "<br />TempOa: ", sprintf("%.1f \u00B0C", tempOa),
                             "<br />Date:       ", time,
                             "<br />Season:  ", season
              )
  ) %>%
  add_markers(data = data %>% filter(season == "Fall"),
              x = ~tempOa,
              y = ~tempR,
              name = "Fall",
              marker = list(color = "#440154", opacity = 0.2),
              hoverinfo = "text",
              text = ~ paste("TempR:   ", sprintf("%.1f \u00B0C", tempR),
                             "<br />TempOa: ", sprintf("%.1f \u00B0C", tempOa),
                             "<br />Date:       ", time,
                             "<br />Season:  ", season
              )
  ) %>%
  add_markers(data = data %>% filter(season == "Winter"),
              x = ~tempOa,
              y = ~tempR,
              name = "Winter",
              marker = list(color = "#365c8d", opacity = 0.2),
              hoverinfo = "text",
              text = ~ paste("TempR:   ", sprintf("%.1f \u00B0C", tempR),
                             "<br />TempOa: ", sprintf("%.1f \u00B0C", tempOa),
                             "<br />Date:       ", time,
                             "<br />Season:  ", season
              )
  ) %>%
  layout(
    xaxis = list(title = "Outside Air Temperature in \u00B0C (Rolling Mean last 48 hours)",
                 range = c(minx, maxx),
                 zeroline = FALSE,
                 tick0 = minx,
                 dtick = 2,
                 titlefont = list(size = 14, color = "darkgrey")),
    yaxis = list(title = "Room Temperature in \u00B0C",
                 range = c(miny, maxy),
                 titlefont = list(size = 14, color = "darkgrey")),
    hoverlabel = list(align = "left"),
    margin = list(l = 80, t = 50, r = 50, b = 10)
  ) %>%
  
  plotly::config(modeBarButtons = list(list("toImage")),
                 displaylogo = FALSE,
                 toImageButtonOptions = list(
                   format = "svg"
                 )
  )

6.5.4 Discussion

The diagram is based on D3 and packaged into the package redutils. The original D3 source with a html integration you can find here: https://github.com/hslu-ige-laes/d3-mollierhx

6.5.5 See Also

If your two time series are in separate files, you must first read them in separately and then merge them into one data frame. See chapter 4.2.4